<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2017/10/12 0012
 * Time: 13:58
 */

namespace Cuke\Library\RateLimit;

/**
 * Redis实现的令牌桶算法
 * @package Cuke\Library\Bucket
 */
class RedisRateLimiter extends RateLimiter
{
    /**
     * RedisBucket constructor.
     * @param \Redis $redis
     * @param string $key 储存令牌的键
     * @param int $timeout 缓存有效期
     * @param int $max_value 最大令牌数量
     * @param double $rate 往令牌桶中放入令牌的速率,也就是每秒钟发放的令牌数量
     * @throws \Exception
     */
    public function __construct($redis,$key,$timeout,$max_value,$rate)
    {
        if(!class_exists('Redis')){
            throw new \Exception('Redis class not exist.');
        }
        if(!($redis instanceof \Redis)){
            throw new \Exception('$redis unrealized \Redis.');
        }
        $this->redis = $redis;
        $this->key = $key;
        $this->timeout = $timeout;
        $this->max_value = $max_value;
        $this->rate = $rate;
        $this->available_key = $key . '.available';
        $this->ts_key = $key . '.ts';
    }

    /**
     * 获取指定个数的令牌
     * @param int $token
     * @return bool
     */
    public function access($token)
    {
        $consumes = false;
        while (true){
            try{
                $this->redis->watch($this->available_key);
                $this->redis->watch($this->ts_key);
                $current_ts = time();
                //获取令牌桶中剩余令牌
                $old_tokens = $this->redis->hGet($this->key,$this->available_key);
                //如果不存在令牌桶，则设置令牌容量为最大容量
                if($old_tokens === false){
                    $current_tokens = $this->max_value;
                }else{
                    $old_ts = $this->redis->hGet($this->key,$this->ts_key);
                    //通过时间戳计算这段时间内应该添加多少令牌，如果桶满，令牌数取桶满数。
                    $current_tokens = floatval($old_tokens) + min(($current_ts - floatval($old_ts))*$this->rate,$this->max_value - floatval($old_tokens));
                }
                //判断剩余令牌是否足够
                if($token>=0 && $token <= $current_tokens){
                    $current_tokens -= $token;
                    $consumes = true;
                }else{
                    $consumes = false;
                }
                //以下动作为更新 redis 中key的值，并跳出循环返回结果。
                $this->redis->multi();
                $this->redis->hSet($this->key,$this->available_key,intval($current_tokens));
                $this->redis->hSet($this->key,$this->ts_key,$current_ts);
                $this->redis->expire($this->key,$this->timeout);
                $this->redis->exec();
                break;

            }catch (\Exception $ex){
                continue;
            }finally{
                $this->redis->unwatch();
            }
        }
        return $consumes;
    }
}